package org.unidata.mdm.rest.v1.meta.service.measurement;

import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.unidata.mdm.core.type.model.MeasurementCategoryElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.GetMeasurementUnitsContext;
import org.unidata.mdm.meta.context.UpsertMeasurementUnitsContext;
import org.unidata.mdm.meta.type.model.measurement.MeasurementUnitsModel;
import org.unidata.mdm.rest.v1.meta.converter.GenericModelInfoConverter;
import org.unidata.mdm.rest.v1.meta.converter.MeasurementUnitsConverter;
import org.unidata.mdm.rest.v1.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.v1.meta.ro.GetGenericModelInfoResultRO;
import org.unidata.mdm.rest.v1.meta.ro.UpsertGenericModelInfoRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.DeleteMeasurementResultRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.GetMeasurementResultRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.GetMeasurementsResultRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.ImportMeasurementResultRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.MeasurementCategoryRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.UpsertMeasurementRequestRO;
import org.unidata.mdm.rest.v1.meta.ro.measurement.UpsertMeasurementResultRO;
import org.unidata.mdm.rest.v1.meta.service.AbstractMetaModelRestService;
import org.unidata.mdm.system.exception.PlatformBusinessException;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * Measurement unit rest controller
 *
 * @author Alexandr Serov
 * @since 24.11.2020
 **/
@Path(MeasurementUnitsRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class MeasurementUnitsRestService extends AbstractMetaModelRestService {

    public static final String SERVICE_PATH = "measurement";

    public static final String SERVICE_TAG = "measurement";

    private static final String MEASUREMENT_CATEGORY_NOT_FOUND_ERROR = "Measurement category %s not found";

    @GET
    @Operation(
        description = "Gets all measurement units, registered for user's storage ID.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetMeasurementsResultRO getAll() {
        Collection<MeasurementCategoryElement> categories = ObjectUtils.defaultIfNull(instance(Descriptors.MEASUREMENT_UNITS).getCategories(), Collections.emptyList());
        GetMeasurementsResultRO result = new GetMeasurementsResultRO();
        result.setMeasurements(categories.stream()
            .map(MeasurementUnitsConverter::to)
            .collect(Collectors.toList())
        );
        return result;
    }

    /**
     * Gets existing MUC by name.
     *
     * @param measurementCategoryName the MUC name
     * @return MUC definition.
     */
    @GET
    @Path("{measurementCategoryName}")
    @Operation(
        description = "Get an existing MUC by name",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetMeasurementResultRO getByName(@Parameter(description = "MUC name.", in = ParameterIn.PATH)
                                            @PathParam("measurementCategoryName") String measurementCategoryName) {
        GetMeasurementResultRO result = new GetMeasurementResultRO();
        result.setMeasurement(
            MeasurementUnitsConverter.to(measurementCategory(measurementCategoryName))
        );
        return result;
    }

    @GET
    @Path("/export")
    @Produces(MediaType.TEXT_XML)
    @Operation(
        description = "Export measurement model.",
        method = HttpMethod.GET,
        parameters = {@Parameter(description = "Storage ID. Optional", in = ParameterIn.QUERY, name = "storageId")},
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = StreamingOutput.class)), responseCode = "200")
        }, tags = SERVICE_TAG)
    public Response exportMeasurement(@QueryParam("storageId") String storageId) {
        return exportXmlFile(SERVICE_TAG, instance(Descriptors.MEASUREMENT_UNITS).toSource());
    }

    @POST
    @Path("/import")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Operation(
        description = "Import measurement model. Only xml is supported.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA), description = "Source file."),
        tags = SERVICE_TAG)
    public ImportMeasurementResultRO importMeasurement(@Multipart(value = "file") Attachment attachment) {
        return importXmlFile(attachment, MeasurementUnitsModel.class, measurementUnitsModel -> {
            ImportMeasurementResultRO result = new ImportMeasurementResultRO();
            result.setStorageId(measurementUnitsModel.getStorageId());
            result.setInstanceId(measurementUnitsModel.getInstanceId());
            metaModelService.upsert(UpsertMeasurementUnitsContext.builder()
                .update(measurementUnitsModel.getValues())
                .storageId(measurementUnitsModel.getStorageId())
                .instanceId(measurementUnitsModel.getInstanceId())
                .waitForFinish(true)
                .build());
            return result;
        });
    }

    @POST
    @Operation(
        description = "Create or update an measurement units category.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(
            schema = @Schema(implementation = UpsertMeasurementRequestRO.class)), description = "Upsert request."),
        tags = SERVICE_TAG)
    public UpsertMeasurementResultRO upsert(UpsertMeasurementRequestRO req) {
        Objects.requireNonNull(req, "Request can't be null");
        MeasurementCategoryRO measurement = notNull("measurement", req.getMeasurement());
        UpsertMeasurementResultRO result = new UpsertMeasurementResultRO();
        metaModelService.upsert(UpsertMeasurementUnitsContext.builder()
                .update(MeasurementUnitsConverter.from(measurement))
                .waitForFinish(true)
                .build());
        result.setName(measurement.getName());
        return result;
    }

    @POST
    @Path("/info")
    @Operation(
        description = "Upserts generic measurement units model info.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UpsertGenericModelInfoRequestRO.class)), description = "Upsert request."),
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO upsert(UpsertGenericModelInfoRequestRO req) {

        metaModelService.upsert(UpsertMeasurementUnitsContext.builder()
                .name(req.getName())
                .displayName(req.getDisplayName())
                .description(req.getDescription())
                .waitForFinish(true)
                .build());

        return GenericModelInfoConverter.to(metaModelService.get(GetMeasurementUnitsContext.builder()
                .modelInfo(true)
                .build()));
    }

    @GET
    @Path("/info")
    @Operation(
        description = "Gets generic measurement units model info.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG)
    public GetGenericModelInfoResultRO get() {
        return GenericModelInfoConverter.to(metaModelService.get(GetMeasurementUnitsContext.builder()
                .modelInfo(true)
                .build()));
    }

    @DELETE
    @Path("{measurementCategoryName}")
    @Operation(
        description = "Delete measurement units category.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG)
    public DeleteMeasurementResultRO delete(@Parameter(description = "MUC name.", in = ParameterIn.PATH)
                                            @PathParam("measurementCategoryName") String measurementCategoryName) {

        metaModelService.upsert(UpsertMeasurementUnitsContext.builder()
            .delete(measurementCategoryName)
            .build());

        DeleteMeasurementResultRO result = new DeleteMeasurementResultRO();
        result.setId(measurementCategoryName);
        return result;
    }

    private MeasurementCategoryElement findMeasurementCategory(String categoryName) {
        return instance(Descriptors.MEASUREMENT_UNITS).getCategory(notNull("enumeration name", categoryName));
    }

    private MeasurementCategoryElement measurementCategory(String categoryName) {
        MeasurementCategoryElement result = findMeasurementCategory(categoryName);
        if (result == null) {
            throw new PlatformBusinessException(String.format(MEASUREMENT_CATEGORY_NOT_FOUND_ERROR, categoryName), MetaRestExceptionIds.EX_META_MEASUREMENT_CATEGORY_NOT_FOUND);
        }
        return result;
    }

}
